iT邦幫忙

2023 iThome 鐵人賽

DAY 19
1

Sealed Interface 是 Java 17 引入的新的特性,而 Kotlin 也滿早就有這樣的功能,今天來討論如何利用 sealed interface 作領域建模。

先談談 Enum

假設我們有一個電影分級制度欄位 AgeRestriction,一般可能會用 String 欄位,那我們可以用 enum 進一步來建模

enum class AgeRestriction(val description: String) {
  General("普遍级"),
  Protect("保護級"),
  PG15("輔導十五歲,未滿十五歲之人不得觀賞"),
  PG12("輔導十二歲,未滿十二歲之兒童不得觀賞"),
  Restricted("限制級")
}

使用 enum class 比使用 String 強大得多。String 有無窮的可能值,但現在我們只有五個可能的值。因此,使用 AgeRestriction 比使用 String 更容易理解和操作。

在函數式程式設計中,這種資料組合也被稱為sum type,其資料模型為"或"關係。所以我們可以說 AgeRestriction 是有限的 1+1+1+1+1 的可能性。這比 String 告訴我們更多。String會有無窮的值,而以 enum class 建模的 AgeRestriction 只有五種不同的值。因此,使用sum type可以大大減少複雜性, 並且增加可能性。

Sealed interface

假設要對一個電影上映進行建模,隨著網路影片平台的興起,會有一種上映事件不是發生在戲院的位址上。而是在某個 Url 上發生的活動。因此,這個 Event 的類型,可能可以這樣簡單地實現它:


@JvmInline value class Url(val value: String)

@JvmInline value class City(val value: String)
@JvmInline value class Street(val value: String)
data class Address(val city: City, val street: Street)

data class Event(
  val id: EventId,
  val title: String,
  val organizer: String,
  val description: String,
  val date: LocalDate,
  val ageRestriction: AgeRestriction,
  val isOnline: Boolean,
  val url: Url?,
  val address: Address?
)

這是一種常見的開發方式,但它可能會有問題。如果isOnline為true,url將為非null,反之亦然。但在檢查isOnline後,url和address仍然是null,所以我們最後得到的代碼像這樣。


fun printLocation(event: Event): Unit =
  if(event.isOnline) {
    event.url?.value?.let(::println)
  } else {
    event.address?.let(::println)
  }

但更糟糕的是,我們也可以輕易地破壞既定的合約,就像下面的例子那樣。


Event(
    Id(0L),
    "Functional Domain Modeling",
    "47 Degrees",
    "Building software with functional DDD...",
    LocalDate.now(),
    AgeRestriction.General,
    true,
    null,
    null
)

因為這樣的型別模型無法限制這樣可能性。即使我們透過 function 說,如果它是 isOnline,那麼 url 將是非null。

這時就可以通過引入 sealed class,將 Event.Online 和 Event.AtAddress 以型別化的方式組合在一起,以防止這個問題。


sealed class Event {
  abstract val id: EventId
  abstract val title: String
  abstract val organizer: String
  abstract val description: String
  abstract val ageRestriction: AgeRestriction
  abstract val date: LocalDate

  data class Online(
    override val id: EventId,
    override val title: String,
    override val organizer: String,
    override val description: String,
    override val ageRestriction: AgeRestriction,
    override val date: LocalDate,
    val url: Url
  ) : Event()

  data class AtAddress(
    override val id: EventId,
    override val title: String,
    override val organizer: String,
    override val description: String,
    override val ageRestriction: AgeRestriction,
    override val date: LocalDate,
    val address: Address
  ) : Event()
}

這解決了先前的問題,可以建立一個沒有Url的線上Event,並提供了一個更好的型別處理方式。現在,我們可以使用精確的方式來比對、處理 Event,由於Kotlin的型別推斷,我們可以安全地在 Event.Online 的情況下訪問 url。


fun printLocation(event: Event): Unit =
  when(event) {
    is Online -> println(event.url.value)
    is AtAddress -> println("${event.address.city}: ${event.address.street}")
  }

這種型別組成方式也被稱為sum type,其模型為"或"關係,但 sealed class 比 enum class提供更強大的功能。sealed class 允許情況存在於物件、數據類或甚至另一個sealed class。enum class不能

個人心得

sealed class 在資料建模時提供了很大型別保證。但是還是要考慮使用的場景,如有 Json 轉換的需要,會需要根據 library 的方式建立特定的 serializer, 如 Jackson。

每日一推 (G)I-DLE

很有百老匯風格的 Nxde
Yes


上一篇
D18: 寫在 JCConf 前 - Coroutines vs Virtual Thread (Project Loom)
下一篇
D20: 寫在 JCconf 前 - Kotlin Data Class 與 Java Records 都是 Product Type
系列文
讓 Kotlin 程式碼更道地 - 談 Effective Kotlin 與相關的 Design Pattern30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言